/*
 * Copyright (C) 2012 Martin Wilsdorf <mwjutils-api@googlecode.com>.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */

package com.google.mwjutils.xcode;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Martin Wilsdorf <mwjutils-api@googlecode.com>
 */
public class XShell {
    public static void mainMain( XShell shell, String ... aArgs ) {
        if ( aArgs.length > 1 ) {
            for ( String arg:aArgs ) {
                if ( arg.length() > 1 ) {
                    if ( arg.charAt(0) == '-' ) {
                        switch ( arg.substring( 1, arg.length() ) ) {
                            case "cmd":
                                String cmdArgs = new String( );
                                if ( aArgs.length > 2 ) {
                                    cmdArgs = aArgs[1] + " " + aArgs[2];
                                }
                                shell.cmd( cmdArgs );
                                break;
                            default:
                                System.out.printf(
                                        "Illegal argument: %s", arg );
                        }
                    }
                }
            }
        } else {
            shell.run();
        }
    }
    public static void main( XShell aChild, String ... aArgs ) {
        XShell shell = new XShell( );
        shell.fChild = aChild;
        mainMain( aChild, aArgs );
    }
    public static void main( String ... aArgs ) {
        XShell shell = new XShell( );
        mainMain( shell, aArgs );
    }

    public XShell( XShell aChild, PrintStream aOut, XLineReader aLineReader ) {
        fOut = aOut;
        fReader = aLineReader;
        fVars.setProperty( "PROMPT", __DEF_PROMPT );
        fVars.setProperty( "USER", "root" );
        fChild = aChild;
    }

    public XShell( XShell aChild ) {
        this( aChild, System.out, new XLineReader( ) );
    }

    public XShell( ) {
        this( null, System.out, new XLineReader( ) );
        fChild = this;
    }

    private String cmd( String aCmd ) {
        String cmd = new String( );
        try {
            if ( aCmd == null ) {
                throw new NullPointerException( );
            }

            if ( aCmd.trim( ).equals( new String( ) ) ) {
                // echo( "-cmd: Empty command." );
                return new String( );
            }
            aCmd = aCmd.trim();
            String[] cmdArgs = new String[] { };
            Method cmdMethod;
            int eqlIndex = aCmd.indexOf( "=" );
            int argIndex = aCmd.indexOf( " " );
            if ( (argIndex == -1 && eqlIndex > 0) || (argIndex > 0 
                    && eqlIndex > 0 && eqlIndex < argIndex ) ) {
                cmdArgs = aCmd.split( "=", 2 );
                setVar( cmdArgs[0], cmdArgs[1] );
            } else {
                if ( argIndex == -1 ) {
                    cmd = aCmd;
                    cmdMethod = fChild.getClass().getMethod( cmd, new Class[] { String[].class } );
                } else {
                    cmd = aCmd.substring( 0, argIndex );
                    cmdMethod = fChild.getClass().getMethod( cmd, new Class[] { String[].class } );
                    cmdArgs = aCmd.substring( argIndex + 1, aCmd.length() )
                            .split( " " );
                }
                cmdMethod.setAccessible( true );
                cmdMethod.invoke( fChild, (Object) cmdArgs);
            }
        } catch ( NoSuchMethodException ex ) {
            error( ex, "cmd", cmd, false );
        } catch ( SecurityException ex ) {
            error( ex, "cmd", cmd );
        } catch ( IllegalAccessException ex ) {
            error( ex, "cmd", cmd );
        } catch ( IllegalArgumentException ex ) {
            error( ex, "cmd", cmd );
        } catch ( InvocationTargetException ex ) {
            error( ex, "cmd", cmd );
        } catch ( NullPointerException ex ) {
            error( ex, "cmd", cmd );
        } catch ( ArrayIndexOutOfBoundsException ex ) {
            error( ex, "cmd", cmd );
        } catch ( StringIndexOutOfBoundsException ex ) {
            error( ex, "cmd", cmd );
        }
        return new String( );
    }

    synchronized public String echo( String ... aArgs ) {
        String echoMessage = new String( );
        for ( String text:aArgs ) {
            echoMessage = echoMessage + text + " ";
        }
        fOut.println( echoMessage );
        return "Text";
    }
    
    synchronized public void echof( String aMsg, String ... aArgs ) {
        fOut.printf( aMsg + "%n", ( Object[] ) aArgs);
    }
    
    public void env( String ... aArgs ) {
        for ( String var:fVars.stringPropertyNames() ) {
            echo( var + "=" + getVar( var ) );
        }
    }
    
    public void error( Exception e, String aBy ) {
        error( e, aBy, new String( ) );
    }
    public void error( Exception e, String aBy, boolean aStack ) {
        error( e, aBy, new String( ), aStack );
    }
    public void error( Exception e, String aBy, String aCmd ) {
        error( e, aBy, aCmd, true );
    }
    synchronized public void error( Exception e, String aBy, String aCmd, boolean aStack ) {
        String errMsg;
        List<String> args = new ArrayList<>( );
        args.add( aBy );
        if ( aCmd.equals( new String( ) ) ) {
            errMsg = "-%s: %s.";
        } else {
            errMsg = "-%s: %s: %s.";
            args.add( aCmd );
        }
        String msg = e.getMessage();
        if ( e instanceof NoSuchMethodException ) {
            msg = "Command not found";
        } else if ( e instanceof SecurityException ) {
            msg = "security exception";
        } else if ( e instanceof IllegalAccessException ) {
            msg = "illegal access";
        } else if ( e instanceof IllegalArgumentException ) {
            msg = "illegal argument";
        } else if ( e instanceof InvocationTargetException ) {
            msg = "invocation target exception";
        } else if ( e instanceof NullPointerException ) {
            msg = "null pointer exception";
        } else if ( e instanceof ArrayIndexOutOfBoundsException ) {
            msg = "array index out of bounds";
        } else if ( e instanceof FileNotFoundException ) {
            msg = "file not found";
        } else if ( e instanceof IOException ) {
            msg = "IO exception";
        } else if ( e instanceof StringIndexOutOfBoundsException ) {
            msg = "string index out of bounds";
        }
        args.add( msg );
        if ( aStack ) {
            errMsg = errMsg + "%n%s";
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            args.add( sw.toString() );
        }
        System.err.printf( errMsg, args.toArray( ( Object[] ) new String[] { } ) );
    }
    
    public void exit( ) {
        fDoExit = true;
    }
    
    public String[] getArgs( String aArgString ) {
        List<String> resultArray = new ArrayList<>( );
        String result = new String( );
        boolean multiLine = false;
        String args = aArgString.trim();
        for( int i = 0; i < args.length( ); i++ ) {
            char c = args.charAt( i );
            switch( c ) {
                case '"':
                    multiLine = !multiLine;
                case ' ':
                    if ( multiLine ) {
                        result = result + c;
                    }
                    else if ( !result.equals( new String( ) ) ) {
                        resultArray.add( result );
                        result = new String( );
                    }
                default:
                    result = result + c;
            }
        }
        if ( multiLine ) {
            boolean nextLine = true;
            do {
                String[] input = prompt( __DEF_PROMPT ).split( "\"", 2 );
                if ( input.length > 0 ) {
                    result = result + input[0];
                    if ( input.length > 1 ) {
                        nextLine = false;
                        String rest[] = getArgs( input[1] );
                        if ( rest.length > 0 ) {
                            result = result + rest[0];
                        }
                        resultArray.add( result );
                        resultArray.addAll( Arrays.asList(rest) );
                    } else {
                        resultArray.add( result );
                    }
                }
            } while ( nextLine );
        }
        return ( String[] ) resultArray.toArray( );
    }

    //<editor-fold defaultstate="collapsed" desc="GETTER">
    /**
     * Get the value of ExitCmd
     *
     * @return the value of ExitCmd
     */
    public String getExitCmd() {
        return fExitOnCmd;
    }
    
    public char getExitChar() {
        return fExitOnChar;
    }
    
    public boolean getMuted( ) {
        return fMuted;
    }
    
    public boolean getUseExitCmd( ) {
        return fUseExitCmd;
    }
    
    public boolean getUseExitChar( ) {
        return fUseExitChar;
    }
    
    public String getVar( String aKey ) {
        return getVar( aKey, new String( ) );
    }
    
    public String getVar( String aKey, String aDefaultValue ) {
        return fVars.getProperty( aKey, aDefaultValue );
    }
    //</editor-fold>
    
    public String[] list( String ... aArgs ) {
        List<String> methods = new ArrayList<>( );
        for ( Method method:getClass().getDeclaredMethods() ) {
            if (Arrays.equals(method.getGenericParameterTypes(), new Class[] { String[].class })) {
                methods.add( method.getName( ) );
            }
        }
        String[] result = new String[] { };
        result = methods.toArray( aArgs );
        Arrays.sort(result);
        for ( String name:result ) {
            echo( name );
        }
        return result;
    }

    private String parseTextVar( String aVarString ) {
        return parseTextVar( aVarString, 0 );
    }
    private String parseTextVar( String aVarString, int aDepth ) {
        String result = new String( );
        for ( int i = 0; i < aVarString.length( ); i++ ) {
            switch ( aVarString.charAt( i ) ) {
                case '<':
                    if ( i < aVarString.length( )-2 ) {
                        String[] var = parseTextVar(
                                aVarString.substring( i + 1 ), aDepth + 1 )
                                .split(":", 2);
                        result = result.concat(
                            fVars.getProperty( var[1], "<" + var[1] + ">" ) );
                        i += Integer.valueOf(var[0]) + 1;
                    }
                    else {
                        return result;
                    }
                    break;
                case '>':
                    if ( aDepth > 0 )
                        return i + ":" + result;
                default:
                    result = result + aVarString.charAt( i );
            }
        }
        return result;
    }

    public String prompt( String promptMessage ) {
        fOut.printf( parseTextVar( promptMessage ) );
        try {
            return fReader.readLine( );
        } catch (FileNotFoundException e) {
            error( e, "XLineReader", "<>" );
        } catch (IOException e) {
            echof( "XLineReader", "<>" );
        }
        return new String( );
    }

    public String run() {
        String input;
        String result = new String( );
        do {
            input = prompt( getVar( "PROMPT" ) );
            if ( input.trim( ).length() > 0 ) {
                if ( fUseExitChar ) {
                    if ( input.trim( ).charAt(0) == __DEF_EXIT_CHAR ) {
                        fDoExit = true;
                    }
                }
                if ( fUseExitCmd ) {
                    if ( input.trim( ).startsWith( __DEF_EXIT_CMD ) ) {
                        fDoExit = true;
                    }
                }
            }
            if ( !fDoExit ) {
                result = cmd( input );
            }
        } while( !fDoExit );
        return result;
    }

    //<editor-fold defaultstate="collapsed" desc="SETTER">
    /**
     * Set the value of ExitCmd
     *
     * @param ExitCmd new value of ExitCmd
     */
    public void setExitCmd(String aExitCmd) {
        fExitOnCmd = aExitCmd;
    }
    
    public void setExitChar(char aExitChar) {
        fExitOnChar = aExitChar;
    }
    
    public void setMuted(boolean aMuted) {
        fMuted = aMuted;
    }
    
    public void setUseExitCmd( boolean aUseExitCmd ) {
        fUseExitCmd = aUseExitCmd;
    }
    
    public void setUseExitChar( boolean aUseExitChar ) {
        fUseExitChar = aUseExitChar;
    }
    
    public synchronized Object setVar( String aKey, String aValue ) {
        return fVars.setProperty( aKey, aValue );
    }
    //</editor-fold>
    
    public final static char   __DEF_EXIT_CHAR = '\0';
    public final static String __DEF_EXIT_CMD = "exit";
    public final static String __DEF_PROMPT = "> ";
    public  Object      fChild;
    private boolean     fDoExit = false;
    private char        fExitOnChar = __DEF_EXIT_CHAR;
    private String      fExitOnCmd = __DEF_EXIT_CMD;
    private boolean     fMuted = false;
    private PrintStream fOut;
    private XLineReader fReader;
    private boolean     fUseExitChar = true;
    private boolean     fUseExitCmd = true;
    private Properties  fVars = new Properties( );
}
